/*******************************************************************************
 * Copyright ©  2011 Hulles Industries LLC <hulles.com>
 * All rights reserved
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *  
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *     
 *******************************************************************************/

package com.hulles.sancho.engine;

import java.text.DateFormat;
import java.util.Properties;
import java.util.UUID;
import java.util.logging.Level;

import javax.mail.Session;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.NoInitialContextException;
import javax.sql.DataSource;

import com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

/**
 * An instance of this class contains application-specific values needed by Sancho Engine
 * @author hulles
 */
public abstract class SanchoApplication {
    @SuppressWarnings("unused")
	private static final Level LOGLEVEL = Level.INFO;
    private String dataSourceMethod = null;
    private String mailSessionMethod = null;
    

    /**
     * Return null in the subclass if you're not using application IDs.
     * @return The ID of the application, as a UUID (not a string).
     */
    public abstract UUID getApplicationID();
    
    /**
     * Are we using a single database connection throughout the application?
     * @return true if we are using a single database connection
     */
    public abstract boolean useSingleConnection();
    
    /**
     * Get the data source for the database connection
     * @return the data source
     * @throws com.hulles.sancho.engine.SanchoApplication.NoDataSourceException
     */
    public abstract DataSource getDataSource() throws NoDataSourceException;
    
    /**
     * Get the javax mail session
     * @return the session
     * @throws com.hulles.sancho.engine.SanchoApplication.NoMailSessionException
     */
    public abstract Session getMailSession() throws NoMailSessionException;
    
    /**
     * Get the MySQL database user name
     * @return the user name
     */
    public abstract String getUserName();
    
    /**
     * Get the MySQL database password
     * @return the password (clear text)
     */
    public abstract String getPassword();
    
    /**
     * Get the name of the MySQL database
     * @return the database name
     */
    public abstract String getDatabaseName();
    
    /**
     * Get the name of the database server ("localhost" e.g.)
     * @return the server name
     */
    public abstract String getServerName();
    
    /**
     * Get the database server port (3306 typically, for MySQL)
     * @return the port
     */
    public abstract int getPort();
    
    /**
     * Get the context for the datasource ("jdbc/B2GDB" e.g.)
     * @return the context
     */
    public abstract String getDataSourceContext();
    
    /**
     * Get the context for the mail session ("jdbc/SanchoMail" e.g.)
     * @return the context
     */
    public abstract String getMailSessionContext();

    /**
     * Create a new instance of SanchoApplication
     */
    public SanchoApplication() {
        DateFormat dateToString = DateFormat.getDateInstance();
        DateFormat timeToString = DateFormat.getTimeInstance();
        String dateString;
        String timeString;
        java.util.Date date;

        date = new java.util.Date();
        dateString = dateToString.format(date);
        timeString = timeToString.format(date);

        Utils.log(Level.INFO, "Starting Up " + dateString + " " + timeString);
        Utils.log(Level.INFO, "Application Name: " + Utils.getAppName());
        Utils.log(Level.INFO, "Version: " + Utils.getAppVersion());
        Utils.log(Level.INFO, "Architecture: " + Utils.osArchitecture());
        Utils.log(Level.INFO, "OS Name: " + Utils.osName());
        Utils.log(Level.INFO, "OS Version: " + Utils.osVersion());
        Utils.log(Level.INFO, "Java Vendor: " + Utils.javaVendor());
        Utils.log(Level.INFO, "Java Version: " + Utils.javaVersion());
        Utils.log(Level.INFO, "Java JRE Installation Dir: " + Utils.javaHome());
        Utils.log(Level.INFO, "Java Home: " + System.getProperty("java.home"));
        Utils.log(Level.INFO, "User Home Directory: " + Utils.userHome());
        Utils.log(Level.INFO, "User Name: " + System.getProperty("user.name"));
        Utils.log(Level.INFO, "PWD: " + System.getenv("PWD"));
    }

    /**
     * Perform a JNDI lookup to get our datasource. The app server can (and should)
     * provide the mechanism by which we do this. Using JNDI allows our application
     * to not have hard-coded specifics of the database connection, but it can be a huge
     * pain in the ass. Tomcat, for example, allows a JNDI lookup as it is normally
     * configured (IF you remember to include the Tomcat library on the class path. And that,
     * ladies and gentlemen, is the result of 6 days of cursing and head-scratching.)
     * Jetty, however, has to be configured to do this. See http://docs.codehaus.org/display/JETTY/JNDI
     * for a way to do this. Here, we try the app server and if we get nuthin' we create our
     * own JNDI resource that we then don't use, returning instead the datasource we created to
     * bind the JNDI resource.
      * @return
     * @throws NoDataSourceException
     */
    public DataSource getJNDIDataSource() throws NoDataSourceException {
    	DataSource source = null;
    	
    	source = getAppServerDataSource();
        dataSourceMethod = "App Server JNDI Data Source";
    	if (source == null) {
    		source = createJNDIDataSource(); // fallback for, e.g., Jetty in Eclipse
        	dataSourceMethod = "Fallback JNDI Data Source";
    	}
        return source;
    }

    /**
     * Perform a JNDI lookup to get our javax mail session. See comments for {@link getJNDIDataSource}
     * @return the new session or null
     * @throws NoMailSessionException
     */
    public Session getJNDIMailSession() throws NoMailSessionException {
    	Session session = null;
    	
    	session = getAppServerMailSession();
        mailSessionMethod = "App Server JNDI Mail Session";
    	if (session == null) {
    		session = createJNDIMailSession(); // fallback for, e.g., Jetty in Eclipse
        	mailSessionMethod = "Fallback JNDI Mail Session";
    	}
        return session;
    }
    
    /**
     * Get a data source from the application server (Tomcat e.g.) Note that to use
     * this, you should define the data source elsewhere, see DbManager for more information
     * @return the data source
     * @throws com.hulles.sancho.engine.SanchoApplication.NoDataSourceException
     */
    private DataSource getAppServerDataSource() throws NoDataSourceException {
        InitialContext initContext;
        Context envContext;
        // source is a BasicDataSource....
        DataSource source = null;

        try {
        	/* If this doesn't work out of the box, and every document everywhere says that
        	 * it should, and it almost never does, make sure you have the Apache Tomcat (e.g.) 
        	 * library on your classpath. And if that doesn't work, 
        	 * ask yourself once again why you're using JNDI
        	 */
            initContext = new InitialContext();
            envContext = (Context) initContext.lookup("java:/comp/env");
            source = (DataSource) envContext.lookup(getDataSourceContext());
        } catch (NoInitialContextException ex) {
            Utils.warning("No app server initial context in getAppServerDataSource");
        	source = null;
        } catch (NamingException ex) {
            Utils.error("Naming error in getAppServerDataSource");
            throw new NoDataSourceException();
        }
        return source;
    }

    public Session getAppServerMailSession() throws NoMailSessionException {
        InitialContext initContext;
        Context envContext;
        Session session = null;

        try {
        	/* If this doesn't work out of the box, and every document everywhere says that
        	 * it should, and it almost never does, make sure you have the Apache Tomcat (e.g.) 
        	 * library on your classpath. And if that doesn't work, 
        	 * ask yourself once again why you're using JNDI
        	 */
            initContext = new InitialContext();
            envContext = (Context) initContext.lookup("java:/comp/env");
            session = (Session) envContext.lookup(getMailSessionContext());
        } catch (NoInitialContextException ex) {
            Utils.warning("No app server initial context in getAppServerMailSession");
        	session = null;
        } catch (NamingException ex) {
            Utils.error("Naming error in getAppServerMailSession");
            throw new NoMailSessionException();
        }
        return session;
    }

    /**
     * Create a pooling data source, bind it in JNDI and return it. You should get it 
     * from the app server with
     * getAppServerDataSource() if possible before trying this. Note that to use
     * this, you should define the data source elsewhere, see DbManager for more information
     * @return the data source
     * @throws com.hulles.sancho.engine.SanchoApplication.NoDataSourceException
     */
    private DataSource createJNDIDataSource() throws NoDataSourceException {
        InitialContext initContext;
        DataSource source = null;
        String[] dsParts;
        
        try {
        	/* Make sure that org.apache.naming.java.javaURLContextFactory is available on 
        	 * your class path AND at runtime, since your server doesn't support JNDI 
        	 * apparently...
        	 */
        	dsParts = getDataSourceContext().split("/");
			System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.naming.java.javaURLContextFactory");
			System.setProperty(Context.URL_PKG_PREFIXES,  "org.apache.naming");            
			initContext = new InitialContext();
			initContext.createSubcontext("java:");
			initContext.createSubcontext("java:/comp");
			initContext.createSubcontext("java:/comp/env");
			initContext.createSubcontext("java:/comp/env/" + dsParts[0]);
			// here we cheat and stack the deck
			source = getPoolingDataSource();
			initContext.bind("java:/comp/env/" + getDataSourceContext(), source);
        } catch (NoInitialContextException ex) {
            Utils.error("No JNDI initial context in createJNDIDataSource");
            throw new NoDataSourceException();
        } catch (NamingException ex) {
            Utils.error("Naming error in createJNDIDataSource");
            throw new NoDataSourceException();
        }
        return source;
    }

    /**
     * Create a mail session, bind it in JNDI and return it. See {@link createJNDIDataSource}
     * @return the mail session
     * @throws com.hulles.sancho.engine.SanchoApplication.NoMailSessionException
     */
    private Session createJNDIMailSession() throws NoMailSessionException {
        InitialContext initContext;
        Session session = null;
        String[] dsParts;
        Properties props;
        
        try {
        	/* Make sure that org.apache.naming.java.javaURLContextFactory is available on 
        	 * your class path AND at runtime, since your server doesn't support JNDI 
        	 * apparently...
        	 */
        	dsParts = getMailSessionContext().split("/");
			System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.naming.java.javaURLContextFactory");
			System.setProperty(Context.URL_PKG_PREFIXES,  "org.apache.naming");            
			initContext = new InitialContext();
			initContext.createSubcontext("java:");
			initContext.createSubcontext("java:/comp");
			initContext.createSubcontext("java:/comp/env");
			initContext.createSubcontext("java:/comp/env/" + dsParts[0]);
			// here we cheat and stack the deck
			// create some properties and get the default Session
			props = new Properties();
			props.put("mail.smtp.host", "localhost");
			session = Session.getDefaultInstance(props);
			initContext.bind("java:/comp/env/" + getMailSessionContext(), session);
        } catch (NoInitialContextException ex) {
            Utils.error("No JNDI initial context in createJNDIMailSession");
            throw new NoMailSessionException();
        } catch (NamingException ex) {
            Utils.error("Naming error in createJNDIMailSession");
            throw new NoMailSessionException();
        }
        return session;
    }

    /**
     * Get a non-pooling data source
     * @return the data source
     */
    public DataSource getNonPoolingDataSource() {
        MysqlDataSource source;

        source = new MysqlDataSource();
        source.setUser(getUserName());
        source.setPassword(getPassword());
        source.setServerName(getServerName());
        source.setPort(getPort());
        source.setDatabaseName(getDatabaseName());
        dataSourceMethod = "NonPooling Data Source";
        return source;
    }

    /**
     * Get a pooling data source
     * @return the data source
     */
    public DataSource getPoolingDataSource() {
        MysqlConnectionPoolDataSource source;

        source = new MysqlConnectionPoolDataSource();
        source.setUser(getUserName());
        source.setPassword(getPassword());
        source.setServerName(getServerName());
        source.setPort(getPort());
        source.setDatabaseName(getDatabaseName());

        source.setAutoReconnect(true);
        source.setAutoReconnectForConnectionPools(true);
//        source.setAutoReconnectForPools(DEBUG);
        dataSourceMethod = "Pooling Data Source";
        return source;
    }
    
    /**
     * Get the name of the method we used to acquire the data source
     * @return the method name
     */
    public String getDataSourceMethod() {
    	return dataSourceMethod;
    }
    
    /**
     * Get the name of the method we used to acquire the mail session
     * @return the method name
     */
    public String getMailSessionMethod() {
    	return mailSessionMethod;
    }
    
    /**
     * This exception serves to remind client apps to deal with the case
     * where there is no data source available
     */
    public static class NoDataSourceException extends Exception {
		private static final long serialVersionUID = 5436502491708569199L;

		/**
         * A simple exception to be thrown if no database connection is available
         */
        public NoDataSourceException() {
            super();
        }
        public NoDataSourceException(Exception ex) {
            super(ex);
        }
    }

    /**
     * This exception serves to remind client apps to deal with the case
     * where there is no mail session available
     */
    public static class NoMailSessionException extends Exception {
		private static final long serialVersionUID = 3171791835960453035L;

		/**
         * A simple exception to be thrown if no database connection is available
         */
        public NoMailSessionException() {
            super();
        }
    }

}
